﻿#region header

//  ***************************************************************************
// *                                LICENSE
//  ***************************************************************************
//  * "Haplous" .NET Simple 2D Game Framework
//  * 
//  * Copyright © Jonathan Evans 2014-2014
//  * 
//  * This program is free software; you can redistribute it and/or
//  * modify it under the terms of the GNU General Public License
//  * as published by the Free Software Foundation; either version 2
//  * of the License, or (at your option) any later version.
//  * 
//  * This program is distributed in the hope that it will be useful,
//  * but WITHOUT ANY WARRANTY; without even the implied warranty of
//  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  * GNU General Public License for more details.
//  * 
//  * You should have received a copy of the GNU General Public License
//  * along with this program; if not, write to the Free Software
//  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//  * 
//  ***************************************************************************
//  * 
//  * Contact: opencollar@googlemail.com
//  * 
//  * Website: http://haplous.codeplex.com/
//  *
//  * NuGet:   http://www.nuget.org/packages/haplous/
//  *
//  * File:    Haplous/Ghosthunt/Ghosthunt.cs
//  *
//  ***************************************************************************

#endregion

using System;

using Ghosthunt.Resources;

using Haplous;

namespace Ghosthunt
{
    /// <summary>
    ///     The main game controller class for the Pac Daddy game.
    /// </summary>
    public class Ghosthunt : Game
    {
        /// <summary>
        ///     The number of lives at the start of the game.
        /// </summary>
        public const int StartingLives = 3;

        /// <summary>
        ///     The length of the countdown at the start of each round, in seconds.
        /// </summary>
        public const int RoundCountdownSeconds = 3;

        /// <summary>
        ///     The duration of each ghost hunt, in seconds.
        /// </summary>
        public const int GhostHuntDurationSeconds = 15;

        /// <summary>
        ///     The number of points awarded when a strawberry is eaten.
        /// </summary>
        public const int StrawberryScore = 10;

        /// <summary>
        ///     The number of points awarded when a cheese is eaten.
        /// </summary>
        public const int CheeseScore = 100;

        /// <summary>
        ///     The number of points awarded when a ghost is eaten.
        /// </summary>
        public const int GhostScore = 1000;

        /// <summary>
        ///     The interval, in milliseconds, between each animation update.
        /// </summary>
        public const int AnimationUpdateIntervalMilliseconds = 100;

        /// <summary>
        ///     A count of the number of strawberries added.
        /// </summary>
        private int _cheeseCount;

        /// <summary>
        ///     The current countdown value.
        /// </summary>
        private int _countdown;

        /// <summary>
        ///     The text to display when counting down a game start.
        /// </summary>
        private TextBlock _countdownText;

        /// <summary>
        ///     The current score.
        /// </summary>
        private int _currentScore = -1;

        /// <summary>
        ///     Indicates that the game is in "Ghost Hunt" mode.
        /// </summary>
        private bool _inGhostHuntMode;

        /// <summary>
        ///     The text block used to display the remaining lives.
        /// </summary>
        private TextBlock _livesText;

        /// <summary>
        ///     The remaining lives.
        /// </summary>
        private int _remainingLives = -1;

        /// <summary>
        ///     The text block used to display the score.
        /// </summary>
        private TextBlock _scoreText;

        /// <summary>
        ///     A count of the number of strawberries added.
        /// </summary>
        private int _strawberryCount;

        /// <summary>
        ///     Initializes a new instance of the <see cref="Ghosthunt" /> class.
        /// </summary>
        public Ghosthunt()
        {
            // Set the size of the game area in "Game Pixels" - in reality the window might be smaller and the graphics will be scaled to fit.
            Size.Set(2000, 1300);
            AddMode("Countdown");
            ActiveMode = AddMode("Normal");
            AddMode("GhostHunt");

            // These synchronizers will be used for animating characters
            Cues.Add(new TimeCue(this, "Animation", new TimeSpan(0, 0, 0, 0, AnimationUpdateIntervalMilliseconds))); // This should fire 20 times a second.
            Cues.Add(new TimeCue(this, "CountdownSynchronizer", new TimeSpan(0, 0, 0, 1))); // A timer that fires once a second.

            // Create the layers.
            AddLayer("Background");
            AddLayer("Maze");
            AddLayer("Strawberries");
            AddLayer("Ghosts");
            AddLayer("Character");
            AddLayer("Text");

            // Set up the visual elements
            AddBackground();
            AddMaze();
            AddStrawberries();
            AddGhosts();
            AddGhosthunter();
            AddText();

            // Initialize modes
            InitializeNormalMode();
            InitializeCountdownMode();

            // Initialize everything ready to start the game.
            CurrentScore = 0;
            RemainingLives = StartingLives;

            StartRound();
        }

        /// <summary>
        ///     Gets or sets the lives text.
        /// </summary>
        /// <value>The lives text.</value>
        internal string LivesText
        {
            get { return _livesText.Text; }
            set { _livesText.Text = value; }
        }

        /// <summary>
        ///     Gets or sets the countdown text.
        /// </summary>
        /// <value>The countdown text.</value>
        internal string CountdownText
        {
            get { return _countdownText.Text; }
            set { _countdownText.Text = value; }
        }

        /// <summary>
        ///     The current score.
        /// </summary>
        public int CurrentScore
        {
            get { return _currentScore; }
            set
            {
                if(OnPropertyChanged("CurrentScore", ref _currentScore, value))
                    _scoreText.Text = "Score: " + _currentScore;
            }
        }

        /// <summary>
        ///     The remaining lives.
        /// </summary>
        public int RemainingLives
        {
            get { return _remainingLives; }
            set
            {
                if(OnPropertyChanged("RemainingLives", ref _remainingLives, value))
                    _livesText.Text = string.Format("Lives: {0}", _remainingLives);
            }
        }

        /// <summary>
        ///     The remaining countdown.
        /// </summary>
        public int Countdown
        {
            get { return _countdown; }
            set
            {
                if(OnPropertyChanged("Countdown", ref _countdown, value))
                    _countdownText.Text = string.Format("{0}", _countdown);
            }
        }

        /// <summary>
        ///     Gets or sets the score text.
        /// </summary>
        /// <value>The score text.</value>
        internal string ScoreText
        {
            get { return _scoreText.Text; }
            set { _scoreText.Text = value; }
        }

        /// <summary>
        ///     Gets the name of this game (used internally in error messages etc.).
        /// </summary>
        /// <value>The name of the game.</value>
        public override string Name
        {
            get { return "Maze"; }
        }

        /// <summary>
        ///     Gets or sets a value indicating whether the game is in "Ghost Hunt" mode.
        /// </summary>
        /// <value><see langword="true" /> if the game is in ghost hunt mode; otherwise, <see langword="false" />.</value>
        public bool InGhostHuntMode
        {
            get { return _inGhostHuntMode; }
            private set { OnPropertyChanged("InGhostHuntMode", ref _inGhostHuntMode, value); }
        }

        /// <summary>
        ///     Adds the ghosts.
        /// </summary>
        private void AddGhosts()
        {
            AddGhost("Red", 130, 200, DirectionKind.Down, DirectionKind.Left); // Top/Left
            AddGhost("Green", 1600, 130, DirectionKind.Right, DirectionKind.Right); // Top/Right
            AddGhost("Blue", 300, 630, DirectionKind.Right, DirectionKind.Right); // Bottom/Left
            AddGhost("Purple", 1810, 1000, DirectionKind.Up, DirectionKind.Left); // Bottom/Right
        }

        /// <summary>
        ///     Starts a new round of the game.
        /// </summary>
        public void StartRound()
        {
            _inGhostHuntMode = false;

            // Update the lives by putting a player on the board.
            RemainingLives = _remainingLives - 1;

            // Now start the countdown.
            ActiveMode = Modes["Countdown"];
            Countdown = RoundCountdownSeconds;
            var synchronizer = Cues["CountdownSynchronizer"];
            synchronizer.Tick += CountdownTick;
        }

        /// <summary>
        ///     Called when the countdown timer ticks.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
        private void CountdownTick(object sender, EventArgs e)
        {
            var countdown = _countdown - 1;
            if(countdown >= 0)
            {
                Countdown = countdown;
                return;
            }

            Cues["CountdownSynchronizer"].Tick -= CountdownTick;

            if(_inGhostHuntMode)
            {
                InGhostHuntMode = false;
                ActiveMode = Modes["Normal"];
                return;
            }

            // Countdown finished - start game
            ActiveMode = Modes["Normal"];
            if(_remainingLives < (StartingLives - 1))
                Reset(); // If this is not the first round then we need to switch everything back to the starting position.
            else
                Snap(); // If this is the first round we need to capture the position of everything before we start.
        }

        /// <summary>
        ///     Initializes the countdown mode.
        /// </summary>
        private void InitializeCountdownMode()
        {
            // Copy the setup from the normal mode.
            CopyModeProperties(Modes["Normal"], Modes["Countdown"], new[] {"Director"});

            ActiveMode = Modes["Countdown"];
            foreach(var element in Elements)
            {
                var block = element as IBlock;
                if((element is Wall) || (element.Name == "Countdown") || (element.Name == "Background"))
                {
                    if(block != null)
                        block.Opacity.Value = 1d;
                }
                else
                {
                    if(block != null)
                        block.Opacity.Value = 0d;
                }
            }
        }

        /// <summary>
        ///     Define the modes (e.g the normal mode when we are running away from ghosts and the "Chase" mode when
        ///     we are trying to eat ghosts)
        /// </summary>
        private void InitializeNormalMode()
        {
            // Set the mode we are initializing.
            ActiveMode = Modes["Normal"];

            foreach(var element in Elements)
            {
                var block = element as IBlock;
                if((element.Name == "Countdown"))
                {
                    if(block != null)
                        block.Opacity.Value = 0d;
                }
                else
                {
                    if(block != null)
                        block.Opacity.Value = 1d;
                }
            }
        }

        /// <summary>
        ///     Adds the background elements.
        /// </summary>
        private void AddBackground()
        {
            var backgroundPicture = AddPicture("Background", ResourcesHelper.GetBitmap("Background"));
            var background = new Background(this, "Background");
            background.PictureSource.Source = backgroundPicture;
            Layers["Background"].Elements.Add(background);
        }

        /// <summary>
        ///     Adds the text elements.
        /// </summary>
        private void AddText()
        {
            const int livesBoxWidth = 200; //pixels

            _livesText = AddText("Lives", 100, 10, livesBoxWidth, 80, "Tahoma", 20, "#FFFFFF"); // Top/Left
            _scoreText = AddText("Score", 1700, 10, 250, 80, "Tahoma", 20, "#FFFFFF"); // Top/Right
            _countdownText = AddText("Countdown", 800, 520, 400, 80, "Tahoma", 30, "#FFFFFF"); // Middle/Middle
        }

        /// <summary>
        ///     Adds the Ghosthunter element.
        /// </summary>
        private void AddGhosthunter()
        {
            var characterLayer = Layers["Character"];
            var Ghosthunter = new Ghosthunter(this);
            Ghosthunter.Position.Set(800, 610);
            characterLayer.Elements.Add(Ghosthunter);
        }

        /// <summary>
        ///     Adds the text block specified.
        /// </summary>
        /// <param name="name">The name of the text block.</param>
        /// <param name="x">The X coordinate in game pixels.</param>
        /// <param name="y">The Y coordinate in game pixels.</param>
        /// <param name="width">The width in game pixels.</param>
        /// <param name="height">The height in game pixels.</param>
        /// <param name="fontFamily">The font family.</param>
        /// <param name="fontSize">The size of the font in game pixels.</param>
        /// <param name="foreground">The foreground color.</param>
        /// <returns>The newly created text block (having added it to the text layer).</returns>
        private TextBlock AddText(string name, int x, int y, int width, int height, string fontFamily, int fontSize, string foreground)
        {
            var textLayer = Layers["Text"];
            var textBox = new TextBlock(this, name);
            textBox.Position.Set(x, y);
            textBox.Size.Set(width, height);
            textBox.FontFamily = fontFamily;
            textBox.FontSize = fontSize;
            textBox.Foreground.Set(foreground);
            textBox.HorizontalAlignment = HorizontalAlignment.Center;
            textLayer.Elements.Add(textBox);
            return textBox;
        }

        /// <summary>
        ///     Adds the ghost of the color specified.
        /// </summary>
        /// <param name="color">The color (e.g. "Red").</param>
        /// <param name="x">The initial X coordinate.</param>
        /// <param name="y">The initial Y coordinate.</param>
        /// <param name="initialDirection">The initial direction in which to move.</param>
        /// <param name="huggingWall">
        ///     The direction, relative to the direction of travel, to which we should always keep a wall.
        ///     Must be either <see cref="DirectionKind.Left" /> or <see cref="DirectionKind.Right" />.
        /// </param>
        /// <exception cref="ArgumentOutOfRangeException">
        ///     Only <see cref="DirectionKind.Left" /> or
        ///     <see cref="DirectionKind.Right" /> are valid values of the <paramref name="huggingWall" /> argument.
        /// </exception>
        private void AddGhost(string color, int x, int y, DirectionKind initialDirection, DirectionKind huggingWall)
        {
            var ghost = new Ghost(this, color, initialDirection, huggingWall);
            ghost.Position.Set(x, y);
            Layers["Ghosts"].Elements.Add(ghost);
        }

        /// <summary>
        ///     Adds the maze layer to the game.
        /// </summary>
        private void AddMaze()
        {
            // 00000000001111111111
            // 01234567890123456789
            // #################### 00
            // #                  # 01
            // # #### ###### #### # 02
            // # #  # #    # #  # # 03
            // # #  # #    # #  # # 04
            // # #### ###### #### # 05
            //                      06
            // # #### ###### #### # 07
            // # #  # #    # #  # # 08
            // # #  # #    # #  # # 09
            // # #### ###### #### # 10
            // #                  # 11
            // #################### 12

            // Frame
            AddWall("WALL-FR-TOP", 0, 0, 2000, 100);
            AddWall("WALL-FR-LFT-T", 0, 100, 100, 500);
            AddWall("WALL-FR-LFT-B", 0, 700, 100, 500);
            AddWall("WALL-FR-BTM", 0, 1200, 2000, 100);
            AddWall("WALL-FR-RGT-T", 1900, 100, 100, 500);
            AddWall("WALL-FR-RGT-B", 1900, 700, 100, 500);

            // Top-left block
            AddWall("WALL-TL-TOP", 200, 200, 400, 100);
            AddWall("WALL-TL-LFT", 200, 300, 100, 200);
            AddWall("WALL-TL-RGT", 500, 300, 100, 200);
            AddWall("WALL-TL-BTM", 200, 500, 400, 100);

            // Bottom-left block
            AddWall("WALL-BL-TOP", 200, 700, 400, 100);
            AddWall("WALL-BL-LFT", 200, 800, 100, 200);
            AddWall("WALL-BL-RGT", 500, 800, 100, 200);
            AddWall("WALL-BL-BTM  ", 200, 1000, 400, 100);

            // Top-middle block
            AddWall("WALL-TM-TOP", 700, 200, 600, 100);
            AddWall("WALL-TM-LFT", 700, 300, 100, 200);
            AddWall("WALL-TM-RGT", 1200, 300, 100, 200);
            AddWall("WALL-TM-BTM", 700, 500, 600, 100);

            // Top-right block
            AddWall("WALL-TR-TOP", 1400, 200, 400, 100);
            AddWall("WALL-TR-LFT", 1400, 300, 100, 200);
            AddWall("WALL-TR-RGT", 1700, 300, 100, 200);
            AddWall("WALL-TR-BTM", 1400, 500, 400, 100);

            // Bottom-middle block
            AddWall("WALL-BM-TOP", 700, 700, 600, 100);
            AddWall("WALL-BM-LFT", 700, 800, 100, 200);
            AddWall("WALL-BM-RGT", 1200, 800, 100, 200);
            AddWall("WALL-BM-BTM", 700, 1000, 600, 100);

            // Bottom-right block
            AddWall("WALL-BR-TOP", 1400, 700, 400, 100);
            AddWall("WALL-BR-LFT", 1400, 800, 100, 200);
            AddWall("WALL-BR-RGT", 1700, 800, 100, 200);
            AddWall("WALL-BR-BTM", 1400, 1000, 400, 100);
        }

        /// <summary>
        ///     Adds the strawberries to the maze.
        /// </summary>
        private void AddStrawberries()
        {
            // Left column
            AddStrawberry(140, 140);
            AddStrawberry(140, 240);
            AddStrawberry(140, 340);
            AddStrawberry(140, 440);
            AddStrawberry(140, 540);
            AddStrawberry(140, 640);
            AddStrawberry(140, 740);
            AddStrawberry(140, 840);
            AddStrawberry(140, 940);
            AddStrawberry(140, 1040);
            AddStrawberry(140, 1140);

            // Right column
            AddStrawberry(1840, 140);
            AddStrawberry(1840, 240);
            AddStrawberry(1840, 340);
            AddStrawberry(1840, 440);
            AddStrawberry(1840, 540);
            AddStrawberry(1840, 640);
            AddStrawberry(1840, 740);
            AddStrawberry(1840, 840);
            AddStrawberry(1840, 940);
            AddStrawberry(1840, 1040);
            AddStrawberry(1840, 1140);

            // Left-middle column
            AddStrawberry(640, 140);
            AddStrawberry(640, 240);
            AddCheese(640, 340);
            AddStrawberry(640, 440);
            AddStrawberry(640, 540);
            AddStrawberry(640, 640);
            AddStrawberry(640, 740);
            AddStrawberry(640, 840);
            AddCheese(640, 940);
            AddStrawberry(640, 1040);
            AddStrawberry(640, 1140);

            // Right-middle column
            AddStrawberry(1340, 140);
            AddStrawberry(1340, 240);
            AddCheese(1340, 340);
            AddStrawberry(1340, 440);
            AddStrawberry(1340, 540);
            AddStrawberry(1340, 640);
            AddStrawberry(1340, 740);
            AddStrawberry(1340, 840);
            AddCheese(1340, 940);
            AddStrawberry(1340, 1040);
            AddStrawberry(1340, 1140);

            // Top Row
            AddStrawberry(240, 140);
            AddStrawberry(340, 140);
            AddStrawberry(440, 140);
            AddStrawberry(540, 140);
            AddStrawberry(640, 140);
            AddStrawberry(740, 140);
            AddStrawberry(840, 140);
            AddStrawberry(940, 140);
            AddStrawberry(1040, 140);
            AddStrawberry(1140, 140);
            AddStrawberry(1240, 140);
            AddStrawberry(1340, 140);
            AddStrawberry(1440, 140);
            AddStrawberry(1540, 140);
            AddStrawberry(1640, 140);
            AddStrawberry(1740, 140);

            // Centre Row
            AddStrawberry(40, 640);

            AddStrawberry(240, 640);
            AddStrawberry(340, 640);
            AddStrawberry(440, 640);
            AddStrawberry(540, 640);

            AddStrawberry(740, 640);
            AddStrawberry(840, 640);
            AddStrawberry(940, 640);
            AddStrawberry(1040, 640);
            AddStrawberry(1140, 640);
            AddStrawberry(1240, 640);

            AddStrawberry(1440, 640);
            AddStrawberry(1540, 640);
            AddStrawberry(1640, 640);
            AddStrawberry(1740, 640);

            AddStrawberry(1940, 640);

            // Bottom Row
            AddStrawberry(240, 1140);
            AddStrawberry(340, 1140);
            AddStrawberry(440, 1140);
            AddStrawberry(540, 1140);
            AddStrawberry(640, 1140);
            AddStrawberry(740, 1140);
            AddStrawberry(840, 1140);
            AddStrawberry(940, 1140);
            AddStrawberry(1040, 1140);
            AddStrawberry(1140, 1140);
            AddStrawberry(1240, 1140);
            AddStrawberry(1340, 1140);
            AddStrawberry(1440, 1140);
            AddStrawberry(1540, 1140);
            AddStrawberry(1640, 1140);
            AddStrawberry(1740, 1140);
        }

        /// <summary>
        ///     Adds a section of wall.
        /// </summary>
        /// <param name="name">The name of the element.</param>
        /// <param name="x">The x coordinate.</param>
        /// <param name="y">The y coordinate.</param>
        /// <param name="width">The width of the wall.</param>
        /// <param name="height">The height of the wall.</param>
        private void AddWall(string name, int x, int y, int width, int height)
        {
            var wall = new Wall(this, name);
            wall.Position.Set(x, y);
            wall.Size.Set(width, height);
            Layers["Maze"].Elements.Add(wall);
        }

        /// <summary>
        ///     Adds a strawberry.
        /// </summary>
        /// <param name="x">The x coordinate.</param>
        /// <param name="y">The y coordinate.</param>
        private void AddStrawberry(int x, int y)
        {
            var wall = new Strawberry(this, "Strawberry_" + ++_strawberryCount);
            wall.Position.Set(x, y);
            Layers["Strawberries"].Elements.Add(wall);
        }

        /// <summary>
        ///     Adds a cheese.
        /// </summary>
        /// <param name="x">The x coordinate.</param>
        /// <param name="y">The y coordinate.</param>
        private void AddCheese(int x, int y)
        {
            var wall = new Cheese(this, "Cheese_" + ++_cheeseCount);
            wall.Position.Set(x, y);
            Layers["Strawberries"].Elements.Add(wall);
        }

        /// <summary>
        ///     Called when a strawberry is eaten.
        /// </summary>
        public void StrawberryEaten()
        {
            CurrentScore = _currentScore + StrawberryScore;
        }

        /// <summary>
        ///     Called when a strawberry is eaten.
        /// </summary>
        public void GhostEaten()
        {
            CurrentScore = _currentScore + GhostScore;
        }

        /// <summary>
        ///     Called when a cheese is eaten.
        /// </summary>
        public void CheeseEaten()
        {
            CurrentScore = _currentScore + CheeseScore;
            InGhostHuntMode = true;

            // Now start the countdown.
            ActiveMode = Modes["GhostHunt"];
            Countdown = GhostHuntDurationSeconds;
            var synchronizer = Cues["CountdownSynchronizer"];
            synchronizer.Tick += CountdownTick;
        }
    }
}